package com.wmada.toolkit.wxreformat;

import com.steadystate.css.dom.*;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.LexicalUnitImpl;
import com.steadystate.css.parser.SACParserCSS3;
import com.wmada.toolkit.common.FileUtils;
import com.wmada.toolkit.common.secure.SHA1Utils;
import com.wmada.toolkit.rest.Downloader;
import com.wmada.toolkit.rest.MimeType;
import com.wmada.toolkit.rest.Response;
import com.wmada.toolkit.rest.utils.UrlUtils;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.*;
import org.jsoup.select.NodeVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.css.CSSRule;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author will.ma
 * @date 2020-11-9
 */
public class HtmlProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(HtmlProcessor.class);

    private final Downloader downloader;

    private final String cachePath;
    private final String imageUrlPrefix;

    public HtmlProcessor(String cachePath, String imageUrlPrefix) {
        this.cachePath = cachePath;
        this.imageUrlPrefix = imageUrlPrefix;
        this.downloader = new Downloader();
    }

    public void process(Article article) {
        Document document = Jsoup.parse(article.getHtml(), article.getUrl());

        article.setFilename(article.getId() + "_" + getTimestamp());

        removeScripts(document);

        bright(document);

        Map<String, String> mapping = pickImageUrls(article, document);

        article.getResourceMapping().putAll(mapping);

        document.outputSettings().prettyPrint(false);
        article.setLocalHtml(document.html());
    }

    private void removeScripts(Document document) {
        List<Node> removeNodes = new ArrayList<>();
        document.head().traverse(new NodeVisitor() {
            @Override
            public void head(Node node, int depth) {
                if ("script".equalsIgnoreCase(node.nodeName())) {
                    removeNodes.add(node);
                }
            }

            @Override
            public void tail(Node node, int depth) {

            }
        });
        document.body().traverse(new NodeVisitor() {
            @Override
            public void head(Node node, int depth) {
                if ("script".equalsIgnoreCase(node.nodeName())) {
                    removeNodes.add(node);
                }
            }

            @Override
            public void tail(Node node, int depth) {

            }
        });
        for (Node removeNode : removeNodes) {
            removeNode.remove();
        }
    }

    private void bright(Document document) {
        document.body().selectFirst("#js_content").removeAttr("style");
    }

    private String getTimestamp() {
        long now = System.currentTimeMillis();
        return Long.toHexString(now);
    }

    private Map<String, String> pickImageUrls(Article article, Document document) {
        String              baseUrl = article.getUrl();
        Map<String, String> cache   = new HashMap<>();

        String  dir         = article.getFilename() + "_files";
        String  urlPrefix   = imageUrlPrefix + dir + "/";
        File    cacheFolder = Paths.get(cachePath, dir).toFile();
        boolean cacheImages = cacheFolder.mkdirs();
        if (!cacheImages) {
            return cache;
        }

        document.head().traverse(new NodeVisitor() {
            @Override
            public void head(Node node, int depth) {
                if ("style".equalsIgnoreCase(node.nodeName()) && node instanceof Element) {
                    Element element = (Element) node;
                    for (DataNode dataNode : element.dataNodes()) {
                        String styleSheet = dataNode.getWholeData();
                        if (StringUtils.isNotBlank(styleSheet)) {
                            dataNode.setWholeData(pickImageUrlsInSheet(styleSheet, cache, baseUrl, urlPrefix, cacheFolder));
                        }
                    }
                }
            }

            @Override
            public void tail(Node node, int depth) {

            }
        });

        document.body().traverse(new NodeVisitor() {
            @Override
            public void head(Node node, int depth) {
                if ("img".equalsIgnoreCase(node.nodeName())) {
                    String src = node.attr("data-src");
                    if (StringUtils.isNotBlank(src)) {
                        node.attr("src", src);
                    }

                    src = node.attr("src");

                    node.attr("src", cacheImage(src, baseUrl, cache, cacheFolder, urlPrefix));
                }

                String styles = node.attr("style");
                if (StringUtils.isNotBlank(styles)) {
                    node.attr("style", pickImageUrlsInStyle(styles, cache, baseUrl, urlPrefix, cacheFolder));
                }

                if ("style".equalsIgnoreCase(node.nodeName()) && node instanceof TextNode) {
                    TextNode textNode   = (TextNode) node;
                    String   styleSheet = textNode.text();
                    if (StringUtils.isNotBlank(styleSheet)) {
                        textNode.text(pickImageUrlsInSheet(styleSheet, cache, baseUrl, urlPrefix, cacheFolder));
                    }
                }
            }

            @Override
            public void tail(Node node, int depth) {

            }
        });
        return cache;
    }

    private String cacheImage(String src, String baseUrl, Map<String, String> cache, File cacheFolder, String urlPrefix) {
        if (StringUtils.isBlank(src)) {
            return null;
        }

        String imageUrl = UrlUtils.getAbsolutePath(src, baseUrl);
        if (StringUtils.isBlank(imageUrl)) {
            return null;
        }

        String targetUrl = imageUrl;
        if (cache.containsKey(imageUrl)) {
            targetUrl = cache.get(imageUrl);
        } else {
            String filename = downloadImage(cacheFolder.getAbsolutePath(), imageUrl);
            if (StringUtils.isNotBlank(filename)) {
                targetUrl = urlPrefix + filename;
                cache.put(imageUrl, targetUrl);
            }
        }
        return targetUrl;
    }

    private void cacheBackgroundImage(CSSStyleDeclarationImpl props, Map<String, String> cache, String baseUrl, String urlPrefix, File cacheFolder) {
        for (Property property : props.getProperties()) {
            if (property.getName().equalsIgnoreCase("background-image")) {
                Object value = ((CSSValueImpl) property.getValue()).getValue();
                if (value instanceof LexicalUnitImpl) {
                    String url = ((LexicalUnitImpl) value).getStringValue();
                    ((LexicalUnitImpl) value).setStringValue(cacheImage(url, baseUrl, cache, cacheFolder, urlPrefix));
                }
            }
        }
    }

    private String pickImageUrlsInStyle(String styles, Map<String, String> cache, String baseUrl, String urlPrefix, File cacheFolder) {
        try {
            InputSource             source = new InputSource(new StringReader(styles));
            CSSOMParser             parser = new CSSOMParser(new SACParserCSS3());
            CSSStyleDeclarationImpl props  = (CSSStyleDeclarationImpl) parser.parseStyleDeclaration(source);

            cacheBackgroundImage(props, cache, baseUrl, urlPrefix, cacheFolder);

            return props.getCssText();
        } catch (IOException e) {
            LOGGER.warn("", e);
            return styles;
        }
    }

    private String pickImageUrlsInSheet(String styles, Map<String, String> cache, String baseUrl, String urlPrefix, File cacheFolder) {
        try {
            InputSource       source = new InputSource(new StringReader(styles));
            CSSOMParser       parser = new CSSOMParser(new SACParserCSS3());
            CSSStyleSheetImpl sheet  = (CSSStyleSheetImpl) parser.parseStyleSheet(source, null, null);
            CSSRuleListImpl   rules  = (CSSRuleListImpl) sheet.getCssRules();
            if (rules.getLength() == 0) {
                return styles;
            }

            for (int i = 0; i < rules.getLength(); i++) {
                CSSRule cssRule = rules.item(i);
                if (cssRule instanceof CSSStyleRuleImpl) {
                    CSSStyleRuleImpl        styleRule = (CSSStyleRuleImpl) cssRule;
                    CSSStyleDeclarationImpl props     = (CSSStyleDeclarationImpl) styleRule.getStyle();
                    cacheBackgroundImage(props, cache, baseUrl, urlPrefix, cacheFolder);
                }
            }

            return sheet.getCssText();
        } catch (IOException e) {
            LOGGER.warn("", e);
            return styles;
        }
    }

    private String downloadImage(String folder, String imageUrl) {
        Response imgPage;
        try {
            imgPage = downloader.get(imageUrl);
        } catch (Exception e) {
            LOGGER.warn("IMAGE URL (Download Error): " + imageUrl);
            return null;
        }

        if (!imgPage.isOk()) {
            LOGGER.warn("IMAGE URL (Download Error): {}\n Because of {}", imageUrl, imgPage.getMessage());
            return null;
        }
        if (imgPage.getBytes().length == 0) {
            LOGGER.warn("IMAGE URL (Content Empty): " + imageUrl);
            return null;
        }

        String   suffix;
        MimeType type = MimeType.getMimeTypeByMime(imgPage.getHeaderValue("Content-Type"));
        if (type != null) {
            suffix = type.getExtension();
        } else {
            suffix = FileUtils.getFileSuffix(imageUrl, "");
        }

        String filename = SHA1Utils.getDigest(imageUrl) + "." + suffix;
        String filePath = FileUtils.getFilePath(folder, filename);

        FileUtils.writeBytes(filePath, imgPage.getBytes());
        return filename;
    }

}
